Chaining Internal Class Methods in Python
Class methods calling each other is perfectly acceptable in general, but this should be done toward the end of making the code easier to follow by encapsulating complexity.  Two things in your example potentially work against that goal:


All of the methods and attributes are public.  (In Python you denote a "private" member by starting its name with _.)
Data is passed between the methods via instance attributes.


This means that someone reviewing this code for possible bugs needs to assume that a caller might call any of these methods while the object is in any arbitrary state -- that's a lot of combinations to consider!

If the only intended usage of this class is the one you've given (create a single instance, call create_repository exactly once), then there isn't much benefit to making it a class with state (since that state is never meant to be reused beyond that one function call).  The other typical way of grouping related functions is to simply define them locally within a larger function:

def create_repository(project_name: str, repo_name: str) -&gt; None:
    """Creates a new repo under the specified project"""

    with open("./repo_request_body.json", "r") as file:
        data = json.load(file)

    def get_project_id() -&gt; str:
        """Returns the Azure DevOps project ID for a project name"""
        _request_object = make_request(
            "_apis/projects/",
            project_name,
            "?api-version=5.1-preview.1",
            request_method = "get"
        )
        return _request_object.get("id")

    def update_project_id() -&gt; None:
        """Updates JSON dictionary with new ID"""
        new_id = get_project_id()
        data["project"]["id"] = new_id

    def update_repo_name() -&gt; None:
        """Updates JSON dictionary with new repo name"""
        data["name"] = repo_name

    json_content_type = {
        'Content-Type': 'application/json',
        'Authorization': 'Basic XXXXXXXXXXXXXXXX'
        }

    update_project_id()
    update_repo_name()
    make_request(
        project_name,
        "/_apis/git/repositories",
        "?api-version=5.1-preview.1",
        request_method = "post",
        data = json.dumps(data),
        headers = json_content_type
    )

create_repository("project_name", "repo_name")


But given that each of these functions is only called once, is there any value to actually giving them individual names and making the reader jump around between them?  I'd probably just write this as one function where everything is written in exactly the order it happens in:

def create_repository(project_name: str, repo_name: str) -&gt; None:
    """Creates a new repo under the specified project"""
    # Load the repo request data from disk.
    with open("./repo_request_body.json", "r") as file:
        data = json.load(file)

    # Get the Azure DevOps project ID for a project name
    _request_object = make_request(
        "_apis/projects/",
        project_name,
        "?api-version=5.1-preview.1",
        request_method="get"
    )
    new_id = _request_object.get("id")

    # Update JSON dictionary with new ID
    data["project"]["id"] = new_id

    # Update JSON dictionary with new repo name
    data["name"] = repo_name

    # Make the server request.
    json_content_type = {
        'Content-Type': 'application/json',
        'Authorization': 'Basic XXXXXXXXXXXXXXXX'
    }
    make_request(
        project_name,
        "/_apis/git/repositories",
        "?api-version=5.1-preview.1",
        request_method="post",
        data=json.dumps(data),
        headers=json_content_type
    )

create_repository("project_name", "repo_name")


IMO this is a lot easier to follow, in that dependencies between the different blocks of code are very obvious and I can read it from top to bottom and easily keep track of what's happening when.

Taking it one step further, I'd eliminate the named variables that only get used once, which shortens the new_id part of the code (again, less for the reader to keep track of if they can see at a glance that function calls chain together to produce a single result, rather than having to try to figure out if a given value is going to be used again later):

def create_repository(project_name: str, repo_name: str) -&gt; None:
    """Creates a new repo under the specified project"""
    # Load the repo request data from disk.
    with open("./repo_request_body.json", "r") as file:
        data = json.load(file)

    # Update JSON dictionary with new ID from server API
    data["project"]["id"] = make_request(
        "_apis/projects/",
        project_name,
        "?api-version=5.1-preview.1",
        request_method="get"
    ).get("id")

    # Update JSON dictionary with new repo name
    data["name"] = repo_name

    # Make the server request to create the repo.
    make_request(
        project_name,
        "/_apis/git/repositories",
        "?api-version=5.1-preview.1",
        request_method="post",
        data=json.dumps(data),
        headers={
            'Content-Type': 'application/json',
            'Authorization': 'Basic XXXXXXXXXXXXXXXX',
        }
    )