Most of these answers work, but they run into problems when dealing with emoji support. As these character may appear as one such as , but under the hood they are actually: +++, if we were to split incorrectly, the emoji might end up broken up.
I wrote up a quick set of tests as this:
    [InlineData(null, 1, "")]
    [InlineData(null, 2, "")]
    [InlineData("    ", 3, "")]
    [InlineData("aaaaa", 1, "a a a a a")]
    [InlineData("aaaaa", 2, "aa aa a")]
    [InlineData("aaaaa", 3, "aaa aa")]
    [InlineData("aaaaa", 4, "aaaa a")]
    [InlineData("aaaaa", 6, "aaaaa")]
    [InlineData("aaaaa", 30, "aaaaa")]
    [InlineData("", 4, " ")]
    [InlineData("a", 4, "a ")]
    [InlineData("aaa", 4, "aaa ")]
    [InlineData("aaa", 4, "aaa ")]
    public void TestAddSpaces(string? value, int numOfCharsBetweenSpaces, string expected)
    {
        ...Add Space Code Here
        Assert.Equal(expected, actual);
    }
Doing just a insert of a space of every X characters based on string.length ended with results like as the emoji was split in the middle:
Assert.Equal() Failure
                  ↓ (pos 4)
    Expected: aaa 
    Actual:   aaa� �
                  ↑ (pos 4)
Assert.Equal() Failure
                  ↓ (pos 4)
    Expected: aaa 
    Actual:   aaa� �  
Using the .Net 5+ answer from this Stack Overflow question: How can I split a Unicode string into multiple Unicode characters in C#? on splitting up the string elements we're able to reliable get spacing inserted in the string in a manner that a user would be expecting.
    public static string AddSpacesSplitText(this string? value, int numOfCharsBetweenSpaces)
    {
        if (string.IsNullOrWhiteSpace(value))
            return string.Empty;
        var elements = SplitIntoTextElements(value);
        string retval = string.Empty;
        for (int i = 0; i <= elements.Length; i += numOfCharsBetweenSpaces)
        {
            retval += string.Join(string.Empty, elements.Skip(i).Take(numOfCharsBetweenSpaces)) + " ";
        }
        return retval.Trim();
    }
    public static string[] SplitIntoTextElements(string input)
    {
        IEnumerable<string> Helper()
        {
            for (var en = StringInfo.GetTextElementEnumerator(input); en.MoveNext();)
                yield return en.GetTextElement();
        }
        return Helper().ToArray();
    }
Now running my test cases
    [Theory]
    [InlineData(null, 1, "")]
    [InlineData(null, 2, "")]
    [InlineData("    ", 3, "")]
    [InlineData("aaaaa", 1, "a a a a a")]
    [InlineData("aaaaa", 2, "aa aa a")]
    [InlineData("aaaaa", 3, "aaa aa")]
    [InlineData("aaaaa", 4, "aaaa a")]
    [InlineData("aaaaa", 6, "aaaaa")]
    [InlineData("aaaaa", 30, "aaaaa")]
    [InlineData("", 4, " ")]
    [InlineData("a", 4, "a ")]
    [InlineData("aaa", 4, "aaa ")]
    [InlineData("aaa", 4, "aaa ")]
    public void TestAddSpacesSplitText(string? value, int numOfCharsBetweenSpaces, string expected)
    {
        var actual = value.AddSpacesSplitText(numOfCharsBetweenSpaces);
        Assert.Equal(expected, actual);
    }
They all now pass as expected.